﻿// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
//
// LocationsPage.xaml.cpp
// Implementation of the LocationsPage class
//

#include "pch.h"
#include "LocationsPage.xaml.h"
#include "DataModel\ForecastManager.h"
#include "ExceptionPolicyFactory.h"
#include "LocationData.h"
#include "MessageDialog.h"
#include "ThumbnailManager.h"

using namespace Weathr;

using namespace Platform;
using namespace Platform::Collections;
using namespace concurrency;
using namespace std;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Graphics::Display;
using namespace Windows::UI::Popups;
using namespace Windows::UI::ViewManagement;
using namespace Windows::UI::Xaml;
using namespace Windows::UI::Xaml::Controls;
using namespace Windows::UI::Xaml::Controls::Primitives;
using namespace Windows::UI::Xaml::Data;
using namespace Windows::UI::Xaml::Input;
using namespace Windows::UI::Xaml::Interop;
using namespace Windows::UI::Xaml::Media;
using namespace Windows::UI::Xaml::Navigation;

// The Items Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=234233

// http://social.msdn.microsoft.com/Forums/windowsapps/en-US/263e267a-7d6c-4478-9679-a2ac1b0e83cc/how-do-i-access-appbar-buttons-programmatically
static AppBarButton^ GetButtonFromCommands(IObservableVector<ICommandBarElement^>^ commands, String^ name)
{
    for (auto iter = begin(commands); iter != end(commands); ++iter)
    {
        ICommandBarElement^ x = *iter;
        auto button = dynamic_cast<AppBarButton^>(x);
        if (button != nullptr)
        {
            if (button->Name == name)
            {
                return button;
            }
        }
    }
    return nullptr;
}

static AppBarButton^ GetButtonFromBar(CommandBar^ bar, String^ name)
{
    auto button = GetButtonFromCommands(bar->PrimaryCommands, name);
    if (button != nullptr)
    {
        return button;
    }
    button = GetButtonFromCommands(bar->SecondaryCommands, name);
    return button;
}

LocationsPage::LocationsPage()
{
    InitializeComponent();
    SetValue(_defaultViewModelProperty, ref new Map<String^, Object^>(std::less<String^>()));
    auto navigationHelper = ref new Common::NavigationHelper(this);
    SetValue(_navigationHelperProperty, navigationHelper);
    navigationHelper->LoadState += ref new Common::LoadStateEventHandler(this, &LocationsPage::LoadState);
    navigationHelper->SaveState += ref new Common::SaveStateEventHandler(this, &LocationsPage::SaveState);

    DefaultViewModel->Insert("HasSelection", false);
}

DependencyProperty^ LocationsPage::_defaultViewModelProperty =
DependencyProperty::Register("DefaultViewModel",
TypeName(IObservableMap<String^, Object^>::typeid), TypeName(LocationsPage::typeid), nullptr);

/// <summary>
/// used as a trivial view model.
/// </summary>
IObservableMap<String^, Object^>^ LocationsPage::DefaultViewModel::get()
{
    return safe_cast<IObservableMap<String^, Object^>^>(GetValue(_defaultViewModelProperty));
}

DependencyProperty^ LocationsPage::_navigationHelperProperty =
DependencyProperty::Register("NavigationHelper",
TypeName(Common::NavigationHelper::typeid), TypeName(LocationsPage::typeid), nullptr);

/// <summary>
/// Gets an implementation of <see cref="NavigationHelper"/> designed to be
/// used as a trivial view model.
/// </summary>
Common::NavigationHelper^ LocationsPage::NavigationHelper::get()
{
    return safe_cast<Common::NavigationHelper^>(GetValue(_navigationHelperProperty));
}

#pragma region Navigation support

/// The methods provided in this section are simply used to allow
/// NavigationHelper to respond to the page's navigation methods.
/// 
/// Page specific logic should be placed in event handlers for the  
/// <see cref="NavigationHelper::LoadState"/>
/// and <see cref="NavigationHelper::SaveState"/>.
/// The navigation parameter is available in the LoadState method 
/// in addition to page state preserved during an earlier session.
void LocationsPage::OnNavigatedTo(NavigationEventArgs^ e)
{
    NavigationHelper->OnNavigatedTo(e);

    // Apply the animation that darkens the background.
    DarkenBackground->Begin();
}

void LocationsPage::OnNavigatedFrom(NavigationEventArgs^ e)
{
    NavigationHelper->OnNavigatedFrom(e);
}

#pragma endregion

/// <summary>
/// Populates the page with content passed during navigation.  Any saved state is also
/// provided when recreating a page from a prior session.
/// </summary>
/// <param name="navigationParameter">The parameter value passed to
/// <see cref="Frame::Navigate(Type, Object)"/> when this page was initially requested.
/// </param>
/// <param name="pageState">A map of state preserved by this page during an earlier
/// session.  This will be null the first time a page is visited.</param>
void LocationsPage::LoadState(Platform::Object^ sender, Common::LoadStateEventArgs^ e)
{
    (void) sender;	// Unused parameter

    vector<LocationItem^> items;
    for (Forecast^ forecast : ForecastManager::Current->AllForecastsSortedAlphabetically)
    {
        items.emplace_back(ref new LocationItem(forecast));
    }


    std::map<char16, LocationGroup^> m;
    char16 strBuf[2];
    strBuf[1] = L'\0';
    for (char16 c = L'A'; c <= L'Z'; ++c)
    {
        strBuf[0] = c;
        m.insert(std::make_pair(c, ref new LocationGroup(ref new String(strBuf))));
    }

    for (LocationItem^ item : items)
    {
        char16 ch;
        auto name = item->Title;
        if (name == nullptr || name->Length() == 0)
        {
            ch = L'?';
        }
        else
        {
            ch = *name->Begin();
        }
        auto iter = m.find(ch);
        if (iter == end(m))
        {
            strBuf[0] = ch;
            iter = m.insert(make_pair(ch, ref new LocationGroup(ref new String(strBuf)))).first;
        }

        auto group = iter->second;
        group->Items->Append(item);
    }

    vector<LocationGroup^> groups;
    for (auto& pr : m)
    {
        groups.emplace_back(pr.second);
    }

    DefaultViewModel->Insert("Groups", ref new Vector<LocationGroup^>(groups));

    (safe_cast<ListViewBase^>(semanticZoom->ZoomedOutView))->ItemsSource = groupsViewSource->View->CollectionGroups;

    // Load any cached page state.
    auto pageState = e != nullptr ? e->PageState : nullptr;
    if (pageState == nullptr)
    {
        return;
    }

    // Remember which view is active.
    if (pageState->HasKey("IsZoomedInViewActive"))
    {
        semanticZoom->IsZoomedInViewActive = safe_cast<IPropertyValue^>(pageState->Lookup("IsZoomedInViewActive"))->GetBoolean();
    }

    // Select any items that were selected.
    if (pageState->HasKey("SelectedItems"))
    {
        auto selectedItems = safe_cast<Map<String^, Object^>^>(pageState->Lookup("SelectedItems"));
        auto v = to_vector(itemGridView->Items);
        for (auto pair : selectedItems)
        {
            auto uid = pair->Key;
            for (auto e: v)
            {
                auto locationItem = safe_cast<LocationItem^>(e);
                if (locationItem->Item->UniqueIdentifier == uid)
                {
                    itemGridView->SelectedItems->Append(locationItem);
                    DefaultViewModel->Insert("HasSelection", true);
                    break;
                }
            }
        }
    }
}

/// <summary>
/// Preserves state associated with this page in case the application is suspended or the
/// page is discarded from the navigation cache.  Values must conform to the serialization
/// requirements of <see cref="SuspensionManager::SessionState"/>.
/// </summary>
/// <param name="pageState">An empty map to be populated with serializable state.</param>
void LocationsPage::SaveState(Object^ sender, Common::SaveStateEventArgs^ e)
{
    (void) sender;	// Unused parameter

    auto pageState = e->PageState;

    // Remember which view is active.
    if (pageState->HasKey("IsZoomedInViewActive"))
    {
        pageState->Remove("IsZoomedInViewActive");
    }
    pageState->Insert("IsZoomedInViewActive", PropertyValue::CreateBoolean(semanticZoom->IsZoomedInViewActive));

    // Remember which locations were selected.
    if (pageState->HasKey("SelectedItems"))
    {
        pageState->Remove("SelectedItems");
    }
    auto selectedItems = ref new Map<String^, Object^>();
    auto v = to_vector(itemGridView->SelectedItems);
    for (auto e : v)
    {
        auto locationItem = safe_cast<LocationItem^>(e);
        selectedItems->Insert(locationItem->Item->UniqueIdentifier, PropertyValue::CreateBoolean(true));
    }
    if (selectedItems->Size > 0)
    {
        pageState->Insert("SelectedItems", selectedItems);
    }
}

void LocationsPage::DeleteSelection_Click(Object^ sender, RoutedEventArgs^ e)
{
    wchar_t buffer[256];
    if (itemGridView->SelectedItems->Size == 1)
    {
        swprintf_s(buffer, L"You are about to delete 1 location.", itemGridView->SelectedItems->Size);
    }
    else
    {
        swprintf_s(buffer, L"You are about to delete %u locations.", itemGridView->SelectedItems->Size);
    }
    MessageDialog::ShowConfirmDialogAsync(ref new String(buffer), "").then([this](DialogResult answer)
    {
        assert(IsMainThread());
        if (answer == DialogResult::OK)
        {
            auto forecastManager = ForecastManager::Current;
            auto v = to_vector(itemGridView->SelectedItems);
            bool defaultWasSelected = false;
            String^ defaultLocationDisplayName;
            for (auto e : v)
            {
                auto item = safe_cast<LocationItem^>(e);
                auto forecast = item->Item;

                // Only remove the item if it's not the default home location.
                if (forecast->UniqueIdentifier == ForecastManager::DefaultHomeForecastUid)
                {
                    defaultWasSelected = true;
                    defaultLocationDisplayName = item->Item->DisplayName;
                }
                else
                {

                    if (forecast->IsHomeLocation)
                    {
                        forecastManager->HomeForecast = forecastManager->FindForecast(ForecastManager::DefaultHomeForecastUid);
                    }

                    // Remove from forecast manager.
                    forecastManager->RemoveForecast(forecast);

                    // Remove from local list.
                    auto groups = safe_cast<Vector<LocationGroup^>^>(DefaultViewModel->Lookup("Groups"));
                    for (auto group : groups)
                    {
                        unsigned int index = 0;
                        for (auto item : group->Items)
                        {
                            if (forecast == item->Item)
                            {
                                group->Items->RemoveAt(index);
                                break;
                            }
                            ++index;
                        }
                    }
                }
            }
            LocationData::Current().SaveUserLocationsAsync();

            // Tell the user we can't delete the default home location.
            if (defaultWasSelected)
            {
                wchar_t buffer[256];
                swprintf_s(buffer, L"Let's keep %s around", defaultLocationDisplayName->Data());
                MessageDialog::ShowAcceptDialogAsync("We need a default place to call home.", ref new String(buffer));
            }

            // Disable Delete button.
            DefaultViewModel->Insert("HasSelection", false);
        }
    });
}

void LocationsPage::ClearSelection_Click(Object^ sender, RoutedEventArgs^ e)
{
    itemGridView->SelectedItems->Clear();

    // Disable Delete button.
    DefaultViewModel->Insert("HasSelection", false);
}

void LocationsPage::Reset_Click(Object^ sender, RoutedEventArgs^ e)
{
    // Because all forecasts will be reset, remember the name of the home forecast so we can set it back later.
    auto forecastManager = ForecastManager::Current;
    String^ homeForecastUid = ForecastManager::Current->HomeForecast != nullptr ? ForecastManager::Current->HomeForecast->UniqueIdentifier : nullptr;
    forecastManager->HomeForecast = nullptr;

    MessageDialog::ShowConfirmDialogAsync("This will reset the locations list to the default.", "").then([this, homeForecastUid](DialogResult answer)
    {
        assert(IsMainThread());
        if (answer == DialogResult::OK)
        {
            auto forecastManager = ForecastManager::Current;

            // Delete each forecast explicitly so that we clean up resources such as thumbnails.
            vector<Forecast^> forecasts;
            for (auto group : forecastManager->ForecastGroups)
            {
                for (auto forecast : group->Items)
                {
                    forecasts.emplace_back(forecast);
                }
            }
            for (auto forecast : forecasts)
            {
                forecastManager->RemoveForecast(forecast);
            }


            LocationData::Current().LoadAppLocationsAsync().then([forecastManager](task<web::json::value> previousTask)
            {
                assert(IsMainThread());
                forecastManager->LoadLocations(previousTask.get());

            }, task_continuation_context::use_current()).then([this, homeForecastUid]()
            {
                assert(IsMainThread());
                LoadState(nullptr, nullptr);
                LocationData::Current().SaveUserLocationsAsync();

                auto forecastManager = ForecastManager::Current;
                forecastManager->HomeForecast = forecastManager->FindForecast(homeForecastUid);
                if (forecastManager->HomeForecast == nullptr)
                {
                    // The previous home forecast was not part of the default set. Set the home to the default.
                    forecastManager->HomeForecast = forecastManager->FindForecast(ForecastManager::DefaultHomeForecastUid);
                }

            }, task_continuation_context::use_current());
        }
    });
}

void LocationsPage::semanticZoom_ViewChangeStarted(Object^ sender, SemanticZoomViewChangedEventArgs^ e)
{
    const bool enabled = !e->IsSourceZoomedInView;
    DefaultViewModel->Insert("HasSelection", enabled && itemGridView->SelectedItems->Size > 0);
    GetButtonFromBar(BottomCommandBar, "ClearSelectionAppBarButton")->IsEnabled = enabled;
}


void LocationsPage::itemGridView_SelectionChanged(Object^ sender, SelectionChangedEventArgs^ e)
{
    DefaultViewModel->Insert("HasSelection", itemGridView->SelectedItems->Size > 0);
}

void LocationsPage::Image_ImageFailed(Object^ sender, ExceptionRoutedEventArgs^ e)
{
    auto forecast = safe_cast<LocationItem^>((safe_cast<Image^>(sender))->DataContext)->Item;

    // Common case is a 0-size or corrupted file. It also may still be open, so wait a bit for it to close and retry.
    forecast->ThumbnailImagePath = nullptr;

    assert(IsMainThread());
    run_async_with_retry<void>(
    [forecast]()
    {
        return ForecastManager::Current->CreateThumbnailAsync(forecast).then([forecast]()
        {
            assert(IsMainThread());
            return ThumbnailManager::GetThumbnailAsync(forecast).then([forecast](Windows::Storage::StorageFile^ file)
            {
                assert(IsMainThread());

                // If the thumbnail file doesn't exist, queue it for creation.
                if (file != nullptr)
                {
                    forecast->ThumbnailImagePath = file->Path;
                }
            }, task_continuation_context::use_current());

        }, task_continuation_context::use_current());
    },
    [](uint32_t retryCount)
    {
        // Retry up to 2 times.
        if (retryCount < 2u)
        {
            // Create a delay before returning.
            return create_delayed_task(std::chrono::seconds(2), []{ return true; });
        }
        else
        {
            // Otherwise, cancel processing as soon as possible.
            return task_from_result(false);
        }
    }).then([](task<void> previousTask)
    {
        try
        {
            previousTask.get();
        }
        // TODO: Investigate race conditions.
        catch (AccessDeniedException^)
        {
        }
    });
}
